前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础篇

    • 环境搭建
    • node部署
    • 基础应用
  • 内置模块

    • 1.0 本地路径处理 path
    • 2.0 文件系统操作 fs
    • 3.1 基础调试 console
    • 3.2 本地调试远程服务器上的Node代码
    • 4.1 网络服务 http
    • 4.2 网络服务 http res
    • 4.3 网络服务 http req
    • 4.4 网络服务 http server
    • 4.5 网络服务 http client
    • 4.6 网络服务 https
    • 4.7 网络TCP net
    • 4.8 网络UDP dgram
    • 4.9 域名解析 dns
    • 5.0 网络地址解析 url
    • 5.1 URL查询字符串 querystring
    • 6.1 流操作 stream
    • 6.2 逐行读取 readline
    • 7.1 进程相关 process
    • 7.2 子进程 child
    • 8.1 二进制数据 buffer
    • 8.2 二进制解码 string_decoder
    • 9.1 事件机制 events
    • 9.2 实用工具模块 util
    • 9.3 数据加密 crypto
    • 9.4 MD5入门介绍及crypto模块的应用
    • 9.4 资源压缩 zlib
    • 9.5 集群 cluster
    • 9.6 v8
  • 进阶篇

    • 5分钟入门非对称加密用法
    • cluster
    • cookie_parser深入
    • crypto模块之理论篇
    • express+cookie_parser签名机制深入剖析
    • express+session实现简易身份认证
    • https
    • log4js入门实例
    • node8_napi
    • 使用 async 控制并发
    • 使用 eventproxy 控制并发
    • 使用 superagent 与 cheerio 完成简单爬虫
    • 基于express+muter的文件上传
    • 将图片转成datauri嵌入到html
    • 常用中间件 body_parser 实现解析
    • 日志模块morgan
    • 服务端字符编解码&乱码处理 charset_enc_dec
    • 测试用例mocha,should,istanbul
    • 调试日志打印debug模块
  • Express

    • 1.1 Express概览篇
    • 1.2 Express
  • Koa2

    • 1.0 koa2概览篇
    • 1.1 快速开始
    • 1.2 async await使用
    • 1.3 koa2简析结构
    • 1.4 koa中间件开发与使用
    • 10.1 单元测试
    • 11.1 开发debug
    • 12.1 快速启动
    • 12.2 框架设计
    • 12.3 分层操作
    • 12.4 数据库设计
    • 12.5 路由设计
    • 12.6 webpack4环境搭建
    • 12.7 使用react
    • 12.8 登录注册功能实现
    • 12.9 session登录态判断处理
    • 13.1 import export使用
    • 2.1 原生koa2实现路由
    • 2.2 koa router中间
    • 3.1 GET请求数据获取
    • 3.2 POST请求数据获取
    • 3.3 koa bodyparser中间件
    • 4.1 原生koa2实现静态资源服务器
    • 4.2 koa static中间件
    • 5.1 koa2使用cookie
    • 5.2 koa2实现session
    • 6.1 koa2加载模板引擎
    • 6.2 ejs模板引擎
    • 7.1 busboy模块
    • 7.2 上传文件简单实现
    • 7.3 异步上传图片实现
    • 8.1 mysql模块
    • 8.2 async await封装使用mysql
    • 8.3 项目建表初始化
    • 9.1 原生koa2实现JSONP
    • 9.2 koa jsonp中间件
  • 其他

    • 操作数据库
    • Session 与 Token
    • Cookie、Session、Token、JWT
    • Socket

# koa2 原生路由实现

30 秒速记

  • Koa 不带路由;原生路由 = 一个中间件里读 ctx.path + ctx.method,用 if / switch / 正则分发
  • 匹配用 ctx.path(不含查询串),别用 ctx.url:/todo?x=1 会对不上
  • 没人设置 ctx.body 时 Koa 默认回 404 Not Found;兜底页要显式设 ctx.status = 404
  • 路径存在但方法不对,应该回 405 并带 Allow 头,不是 404
  • 手写适合理解原理;有 /user/:id 参数、分组、方法区分时就上 @koa/router

Koa 本身不带路由,原生路由就是在中间件里自己看请求路径,再决定返回什么。 最简单的写法是读 ctx.path,用 switch 或正则去匹配,命中了就给 ctx.body 赋值,没命中走兜底。我一般会提醒两件事:匹配用 ctx.path 而不是 ctx.url,不然带查询参数的请求全进 404;另外要把请求方法也算进去,GET /todo 和 POST /todo 通常是两回事。手写几条还好,路由一多、出现 /user/:id 这种参数,就该换路由库了。

理解「koa2 原生路由实现」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 middleware 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

把路径和方法都考虑进去,一个完整一点的原生路由大概是这样,404、405 都处理到了:

const Koa = require('koa')
const app = new Koa()

const routes = {
  '/': { GET: ctx => { ctx.body = '首页' } },
  '/todo': {
    GET: ctx => { ctx.body = '待办列表' },
    POST: ctx => { ctx.status = 201; ctx.body = '已创建' },
  },
}

app.use(async ctx => {
  const route = routes[ctx.path]          // 用 path,不受查询串影响
  if (!route) {
    ctx.status = 404
    ctx.body = '页面不存在'
    return
  }
  const handler = route[ctx.method]
  if (!handler) {
    ctx.status = 405
    ctx.set('Allow', Object.keys(route).join(', '))  // 告诉客户端支持哪些方法
    return
  }
  await handler(ctx)
})

app.listen(3000)
// curl localhost:3000/todo?x=1        -> 200 待办列表
// curl -X DELETE localhost:3000/todo  -> 405,响应头 Allow: GET, POST
// curl localhost:3000/abc             -> 404 页面不存在
@前端进阶之旅: 代码已经复制到剪贴板

这张 routes 表其实就是路由库的雏形。路由库在此基础上多做了几件事:路径参数(/user/:id)、前缀和嵌套、路由级中间件、自动处理 HEAD 和 OPTIONS。理解了这一版,再看 koa-router 的源码会轻松很多。

面试官追问

追问 1/todo 能打开,/todo?from=home 却是 404 页面,为什么?
参考回答

匹配用的是 ctx.url,它带着查询串,'/todo?from=home' !== '/todo'。换成 ctx.path,查询参数从 ctx.query 单独拿。

追问 2一个路由都没命中、也没写兜底,Koa 会返回什么?
参考回答

404,响应体是 Not Found。Koa 在 ctx.body 从没被赋值时默认就是 404。反过来也要小心:兜底分支只要给 body 赋了值,状态就自动变成 200,所以兜底要连 ctx.status = 404 一起写。

追问 3/user/123 这种带 id 的路径,手写要怎么匹配?
参考回答

用正则把参数抠出来:const m = ctx.path.match(/^\/user\/(\d+)$/),m[1] 就是 '123',注意是字符串。写到第三四个这种路由时就该换 @koa/router 了,它直接给你 ctx.params.id。

追问 4DELETE /todo 返回了 404,这样处理对吗?
参考回答

不对。路径是存在的,只是不支持这个方法,应该回 405 Method Not Allowed,并在 Allow 头里写上 GET, POST,客户端一看就知道是方法用错了。koa-router 的 allowedMethods() 干的就是这件事。

追问 5路由测试全过了,但 CI 进程一直不退出,可能是什么原因?
参考回答

服务 listen 了没关。测试里用 const server = app.listen(0) 拿随机端口,结束时 server.close();更省事的是用 supertest(app.callback()),根本不占端口。

# koa2 原生路由实现

# 简单例子

30 秒速记

  • ctx.request.url(简写 ctx.url)= 路径 + 查询串,比如 /hello/world?from=nav
  • 只要路径用 ctx.path,要参数用 ctx.query / ctx.querystring
  • 原生路由的本质:拿到路径 → if / 正则判断 → 给 ctx.body 赋值
  • Koa 按 body 类型自动设 Content-Type:普通字符串 text/plain,< 开头的字符串 text/html,对象 JSON

这个例子就说明一件事:当前访问的地址就在 ctx.request.url 上,路由无非是拿它做判断。 访问 /hello/world,页面原样输出 /hello/world。要留意它带着查询串,访问 /hello/world?from=nav 输出的就是完整这一串,所以真正做匹配时我会用 ctx.path,参数另外从 ctx.query 拿。ctx.url 就是 ctx.request.url 的简写,Koa 把 request 上常用的属性都代理到了 ctx 上,平时直接写 ctx.xxx 就行。

const Koa = require('koa')
const app = new Koa()

app.use( async ( ctx ) => {
  let url = ctx.request.url
  ctx.body = url
})
app.listen(3000)
@前端进阶之旅: 代码已经复制到剪贴板

访问 http://localhost:3000/hello/world 页面会输出 /hello/world,也就是说上下文的请求request对象中url之就是当前访问的路径名称,可以根据ctx.request.url 通过一定的判断或者正则匹配就可以定制出所需要的路由。

面试官追问

追问 1访问 /hello/world?from=nav#top,页面会输出什么?
参考回答

输出 /hello/world?from=nav,#top 不会出现。哈希只在浏览器里用,根本不会发到服务端。

追问 2以 /a/b?x=1 为例,ctx.url、ctx.path、ctx.querystring 分别是什么?
参考回答

ctx.url 是 '/a/b?x=1',ctx.path 是 '/a/b',ctx.querystring 是 'x=1',不带问号;带问号的是 ctx.search。

追问 3为什么 ctx.body = url 显示的是纯文本,不是 HTML?
参考回答

Koa 看 body 的内容自动设类型:字符串以 < 开头判成 text/html,否则是 text/plain;对象会序列化成 application/json。想强制按 HTML 返回就写 ctx.type = 'html'。

追问 4基于这个写法做首页、待办页两个页面,最少要补什么?
参考回答

按 ctx.path 判断,每个分支设好 ctx.body,最后一定要有兜底分支,设 ctx.status = 404。不然随便输个地址都把路径回显出来,看着还像页面正常打开了。

追问 5ctx.url 和 ctx.req.url 有区别吗?
参考回答

值一样,来源不同。ctx.url 是 Koa 封装的 request 上的属性,ctx.req.url 是 Node 原生 IncomingMessage 上的。业务代码用前者;如果有中间件改写过 ctx.url,想看最初的地址用 ctx.originalUrl。

# 定制化的路由

30 秒速记

  • 完整示例是 route-simple:switch 匹配路径 → 读 view 下对应的 HTML → 写进 ctx.body
  • 映射关系:/、/index → index.html;/todo → todo.html;其他 → 404.html
  • 读文件包成 Promise,中间件里 await 拿内容;现在直接用 fs.promises.readFile
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础篇

    • 环境搭建
    • node部署
    • 基础应用
  • 内置模块

    • 1.0 本地路径处理 path
    • 2.0 文件系统操作 fs
    • 3.1 基础调试 console
    • 3.2 本地调试远程服务器上的Node代码
    • 4.1 网络服务 http
    • 4.2 网络服务 http res
    • 4.3 网络服务 http req
    • 4.4 网络服务 http server
    • 4.5 网络服务 http client
    • 4.6 网络服务 https
    • 4.7 网络TCP net
    • 4.8 网络UDP dgram
    • 4.9 域名解析 dns
    • 5.0 网络地址解析 url
    • 5.1 URL查询字符串 querystring
    • 6.1 流操作 stream
    • 6.2 逐行读取 readline
    • 7.1 进程相关 process
    • 7.2 子进程 child
    • 8.1 二进制数据 buffer
    • 8.2 二进制解码 string_decoder
    • 9.1 事件机制 events
    • 9.2 实用工具模块 util
    • 9.3 数据加密 crypto
    • 9.4 MD5入门介绍及crypto模块的应用
    • 9.4 资源压缩 zlib
    • 9.5 集群 cluster
    • 9.6 v8
  • 进阶篇

    • 5分钟入门非对称加密用法
    • cluster
    • cookie_parser深入
    • crypto模块之理论篇
    • express+cookie_parser签名机制深入剖析
    • express+session实现简易身份认证
    • https
    • log4js入门实例
    • node8_napi
    • 使用 async 控制并发
    • 使用 eventproxy 控制并发
    • 使用 superagent 与 cheerio 完成简单爬虫
    • 基于express+muter的文件上传
    • 将图片转成datauri嵌入到html
    • 常用中间件 body_parser 实现解析
    • 日志模块morgan
    • 服务端字符编解码&乱码处理 charset_enc_dec
    • 测试用例mocha,should,istanbul
    • 调试日志打印debug模块
  • Express

    • 1.1 Express概览篇
    • 1.2 Express
  • Koa2

    • 1.0 koa2概览篇
    • 1.1 快速开始
    • 1.2 async await使用
    • 1.3 koa2简析结构
    • 1.4 koa中间件开发与使用
    • 10.1 单元测试
    • 11.1 开发debug
    • 12.1 快速启动
    • 12.2 框架设计
    • 12.3 分层操作
    • 12.4 数据库设计
    • 12.5 路由设计
    • 12.6 webpack4环境搭建
    • 12.7 使用react
    • 12.8 登录注册功能实现
    • 12.9 session登录态判断处理
    • 13.1 import export使用
    • 2.1 原生koa2实现路由
    • 2.2 koa router中间
    • 3.1 GET请求数据获取
    • 3.2 POST请求数据获取
    • 3.3 koa bodyparser中间件
    • 4.1 原生koa2实现静态资源服务器
    • 4.2 koa static中间件
    • 5.1 koa2使用cookie
    • 5.2 koa2实现session
    • 6.1 koa2加载模板引擎
    • 6.2 ejs模板引擎
    • 7.1 busboy模块
    • 7.2 上传文件简单实现
    • 7.3 异步上传图片实现
    • 8.1 mysql模块
    • 8.2 async await封装使用mysql
    • 8.3 项目建表初始化
    • 9.1 原生koa2实现JSONP
    • 9.2 koa jsonp中间件
  • 其他

    • 操作数据库
    • Session 与 Token
    • Cookie、Session、Token、JWT
    • Socket
完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

koa2 原生路由实现|Nodejs专题系列

# koa2 原生路由实现

30 秒速记

  • Koa 不带路由;原生路由 = 一个中间件里读 ctx.path + ctx.method,用 if / switch / 正则分发
  • 匹配用 ctx.path(不含查询串),别用 ctx.url:/todo?x=1 会对不上
  • 没人设置 ctx.body 时 Koa 默认回 404 Not Found;兜底页要显式设 ctx.status = 404
  • 路径存在但方法不对,应该回 405 并带 Allow 头,不是 404
  • 手写适合理解原理;有 /user/:id 参数、分组、方法区分时就上 @koa/router

Koa 本身不带路由,原生路由就是在中间件里自己看请求路径,再决定返回什么。 最简单的写法是读 ctx.path,用 switch 或正则去匹配,命中了就给 ctx.body 赋值,没命中走兜底。我一般会提醒两件事:匹配用 ctx.path 而不是 ctx.url,不然带查询参数的请求全进 404;另外要把请求方法也算进去,GET /todo 和 POST /todo 通常是两回事。手写几条还好,路由一多、出现 /user/:id 这种参数,就该换路由库了。

理解「koa2 原生路由实现」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 middleware 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

把路径和方法都考虑进去,一个完整一点的原生路由大概是这样,404、405 都处理到了:

const Koa = require('koa')
const app = new Koa()

const routes = {
  '/': { GET: ctx => { ctx.body = '首页' } },
  '/todo': {
    GET: ctx => { ctx.body = '待办列表' },
    POST: ctx => { ctx.status = 201; ctx.body = '已创建' },
  },
}

app.use(async ctx => {
  const route = routes[ctx.path]          // 用 path,不受查询串影响
  if (!route) {
    ctx.status = 404
    ctx.body = '页面不存在'
    return
  }
  const handler = route[ctx.method]
  if (!handler) {
    ctx.status = 405
    ctx.set('Allow', Object.keys(route).join(', '))  // 告诉客户端支持哪些方法
    return
  }
  await handler(ctx)
})

app.listen(3000)
// curl localhost:3000/todo?x=1        -> 200 待办列表
// curl -X DELETE localhost:3000/todo  -> 405,响应头 Allow: GET, POST
// curl localhost:3000/abc             -> 404 页面不存在
@前端进阶之旅: 代码已经复制到剪贴板

这张 routes 表其实就是路由库的雏形。路由库在此基础上多做了几件事:路径参数(/user/:id)、前缀和嵌套、路由级中间件、自动处理 HEAD 和 OPTIONS。理解了这一版,再看 koa-router 的源码会轻松很多。

面试官追问

追问 1/todo 能打开,/todo?from=home 却是 404 页面,为什么?
参考回答

匹配用的是 ctx.url,它带着查询串,'/todo?from=home' !== '/todo'。换成 ctx.path,查询参数从 ctx.query 单独拿。

追问 2一个路由都没命中、也没写兜底,Koa 会返回什么?
参考回答

404,响应体是 Not Found。Koa 在 ctx.body 从没被赋值时默认就是 404。反过来也要小心:兜底分支只要给 body 赋了值,状态就自动变成 200,所以兜底要连 ctx.status = 404 一起写。

追问 3/user/123 这种带 id 的路径,手写要怎么匹配?
参考回答

用正则把参数抠出来:const m = ctx.path.match(/^\/user\/(\d+)$/),m[1] 就是 '123',注意是字符串。写到第三四个这种路由时就该换 @koa/router 了,它直接给你 ctx.params.id。

追问 4DELETE /todo 返回了 404,这样处理对吗?
参考回答

不对。路径是存在的,只是不支持这个方法,应该回 405 Method Not Allowed,并在 Allow 头里写上 GET, POST,客户端一看就知道是方法用错了。koa-router 的 allowedMethods() 干的就是这件事。

追问 5路由测试全过了,但 CI 进程一直不退出,可能是什么原因?
参考回答

服务 listen 了没关。测试里用 const server = app.listen(0) 拿随机端口,结束时 server.close();更省事的是用 supertest(app.callback()),根本不占端口。

# koa2 原生路由实现

# 简单例子

30 秒速记

  • ctx.request.url(简写 ctx.url)= 路径 + 查询串,比如 /hello/world?from=nav
  • 只要路径用 ctx.path,要参数用 ctx.query / ctx.querystring
  • 原生路由的本质:拿到路径 → if / 正则判断 → 给 ctx.body 赋值
  • Koa 按 body 类型自动设 Content-Type:普通字符串 text/plain,< 开头的字符串 text/html,对象 JSON

这个例子就说明一件事:当前访问的地址就在 ctx.request.url 上,路由无非是拿它做判断。 访问 /hello/world,页面原样输出 /hello/world。要留意它带着查询串,访问 /hello/world?from=nav 输出的就是完整这一串,所以真正做匹配时我会用 ctx.path,参数另外从 ctx.query 拿。ctx.url 就是 ctx.request.url 的简写,Koa 把 request 上常用的属性都代理到了 ctx 上,平时直接写 ctx.xxx 就行。

const Koa = require('koa')
const app = new Koa()

app.use( async ( ctx ) => {
  let url = ctx.request.url
  ctx.body = url
})
app.listen(3000)
@前端进阶之旅: 代码已经复制到剪贴板

访问 http://localhost:3000/hello/world 页面会输出 /hello/world,也就是说上下文的请求request对象中url之就是当前访问的路径名称,可以根据ctx.request.url 通过一定的判断或者正则匹配就可以定制出所需要的路由。

面试官追问

追问 1访问 /hello/world?from=nav#top,页面会输出什么?
参考回答

输出 /hello/world?from=nav,#top 不会出现。哈希只在浏览器里用,根本不会发到服务端。

追问 2以 /a/b?x=1 为例,ctx.url、ctx.path、ctx.querystring 分别是什么?
参考回答

ctx.url 是 '/a/b?x=1',ctx.path 是 '/a/b',ctx.querystring 是 'x=1',不带问号;带问号的是 ctx.search。

追问 3为什么 ctx.body = url 显示的是纯文本,不是 HTML?
参考回答

Koa 看 body 的内容自动设类型:字符串以 < 开头判成 text/html,否则是 text/plain;对象会序列化成 application/json。想强制按 HTML 返回就写 ctx.type = 'html'。

追问 4基于这个写法做首页、待办页两个页面,最少要补什么?
参考回答

按 ctx.path 判断,每个分支设好 ctx.body,最后一定要有兜底分支,设 ctx.status = 404。不然随便输个地址都把路径回显出来,看着还像页面正常打开了。

追问 5ctx.url 和 ctx.req.url 有区别吗?
参考回答

值一样,来源不同。ctx.url 是 Koa 封装的 request 上的属性,ctx.req.url 是 Node 原生 IncomingMessage 上的。业务代码用前者;如果有中间件改写过 ctx.url,想看最初的地址用 ctx.originalUrl。

# 定制化的路由

30 秒速记

  • 完整示例是 route-simple:switch 匹配路径 → 读 view 下对应的 HTML → 写进 ctx.body
  • 映射关系:/、/index → index.html;/todo → todo.html;其他 → 404.html
  • 读文件包成 Promise,中间件里 await 拿内容;现在直接用 fs.promises.readFile
  • 能看清路由本质,但方法区分、状态码、异常处理都要自己补

定制化路由就是把「路径 → 页面文件」的映射写死在代码里,命中哪个就读哪个 HTML 返回。 这个示例用 switch 判断路径,/ 和 /index 都对应 index.html,/todo 对应 todo.html,其余全落到 404.html。读文件是异步的,所以用 Promise 包了一层,中间件里直接 await。看懂它再去看 koa-router,会发现路由库也就是把这张映射表做得更通用:支持参数、方法、前缀和中间件。

demo源码

https://github.com/poetries/daily-code-practice/tree/master/node/koa/koa2-demo/route-simple

面试官追问

追问 1为什么 render 要包一层 Promise,直接用回调版 readFile 不行吗?
参考回答

回调版的结果在回调里,async 中间件早就执行完返回了,那时 ctx.body 还是空的,Koa 直接回 404。包成 Promise 才能 await 住;现在不用自己包,require('fs').promises.readFile 就是现成的。

追问 2页面涨到二三十个,还用 switch 吗?
参考回答

先换成映射对象 { '/': 'index.html', '/todo': 'todo.html' } 顶一阵;一旦有参数、权限、不同方法,就直接上 @koa/router,没必要自己再造一遍。

追问 3想让 /todo/ 也能打开待办页,怎么改?
参考回答

匹配前统一去掉末尾斜杠:const p = ctx.path.replace(/\/+$/, '') || '/'。更规范的做法是对 /todo/ 做 301 跳到 /todo,免得同一个页面有两个地址。

追问 4能不能直接把路径拼成文件名去读,省掉 switch?
参考回答

千万别。readFile('./view' + ctx.path) 碰到 /../../etc/passwd 就是路径穿越,能读到服务器上任意文件。要么用白名单映射,要么 path.resolve 之后检查结果是否还在 view 目录里。

# 源码文件目录

← 13.1 import export使用2.2 koa router中间 →

fe